package org.bigk.invoices.services;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.bigk.invoices.exceptions.ServiceException;
import org.bigk.invoices.model.Invoice;
import org.bigk.invoices.model.InvoiceFilter;
import org.bigk.invoices.model.InvoicePosition;
import org.bigk.invoices.model.InvoicePositionSummary;
import org.bigk.invoices.model.InvoicePurchaser;
import org.bigk.invoices.model.Invoice_;
import org.bigk.invoices.model.PaymentKind;
import org.bigk.invoices.model.Purchaser;
import org.bigk.invoices.model.PurchaserRole;
import org.bigk.invoices.model.Tax;
import org.bigk.invoices.services.taxesconfig.TaxConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import pl.bigk.utils.lexengines.LexEngine;
import pl.bigk.utils.lexengines.LexEnginePL;


@Service("invoicesService")
@Transactional
public class InvoicesServiceImpl implements InvoicesService {

	private static final Logger logger = LoggerFactory.getLogger(InvoicesServiceImpl.class);

	private static final String ALL_INVOICES_QUERY =
			"SELECT i FROM Invoice i ORDER BY p.year ASC, p.number ASC";

	@PersistenceContext
	protected EntityManager em;

	@Autowired
	protected PaymentKindsService paymentKindsService;
	
	@Autowired
	protected TaxesService taxesService;
	
	@Autowired
	protected PurchasersRolesService purchasersRolesService;
	
	@Autowired
	protected HRInvoiceNumberService hrInvoiceNumberService;
	
	@Autowired
	protected RecalcTotalOfIPSService recalcTotalOfIPSService;
	
	@Autowired
	private TaxesConfigService taxesConfigService;
	
	@Resource(name="invoiceTemplate")
	protected Invoice invoiceTemplate;

	@Override
	public List<Invoice> listAllItems() throws ServiceException {
		logger.debug("listAllItems() - start");

		TypedQuery<Invoice> query = em.createQuery(ALL_INVOICES_QUERY, Invoice.class);
		List<Invoice> list = query.getResultList();

		if (list != null) {
			for (Invoice invoice : list) {
				recalculateInvoice(invoice);
				hrInvoiceNumberService.updateHRInvoiceNumber(invoice);
			}
		}

		logger.debug("listAllItems() - end - read [{}] element(s)", CollectionUtils.size(list));
		return list;
	}

	@Override
	public List<Invoice> listItems4Page(InvoiceFilter filter) throws ServiceException {
		List<Invoice> list = readListForFilter(filter);
		updateFilterStats(filter);
		
		if (list != null) {
			for (Invoice invoice : list) {
				recalculateInvoice(invoice);
				hrInvoiceNumberService.updateHRInvoiceNumber(invoice);
			}
		}
		
		return list;
	}
	
	private List<Invoice> readListForFilter(InvoiceFilter filter) {
		logger.debug("listItems4Page(filter=[{}]) - start", filter);
		
		CriteriaBuilder critBuilder = em.getCriteriaBuilder();
		CriteriaQuery<Invoice> critQuery = critBuilder.createQuery(Invoice.class);
		
		// select-clause
		Root<Invoice> root = critQuery.from(Invoice.class);
		critQuery.select(root);
		
        // where-clause
        Predicate wherePredicate = whereClauseForFilter(critBuilder, critQuery, root, filter);
        if (wherePredicate != null) {
            critQuery.where(wherePredicate);
        }
        
        // order-by-clause
		critQuery.orderBy(Arrays.asList(
				critBuilder.desc(root.get(Invoice_.year)),
				critBuilder.desc(root.get(Invoice_.number))));
        
		TypedQuery<Invoice> query = em.createQuery(critQuery);
		
		query.setFirstResult(filter.calculateFirstResultIndex());
		query.setMaxResults(filter.getPaginatedPageSize());
		
		List<Invoice> list = query.getResultList();
		logger.debug("listItems4Page() - end - read [{}] element(s)", CollectionUtils.size(list));
		return list;
	}
	
	private void updateFilterStats(InvoiceFilter filter) {
		logger.debug("countItems4Page(filter=[{}]) - start", filter);
		
		CriteriaBuilder critBuilder = em.getCriteriaBuilder();
		CriteriaQuery<Number> critQuery = critBuilder.createQuery(Number.class);
		
		// select-clause
		Root<Invoice> root = critQuery.from(Invoice.class);
		critQuery.select(critBuilder.count(root));
		
        // where-clause
        Predicate wherePredicate = whereClauseForFilter(critBuilder, critQuery, root, filter);
        if (wherePredicate != null) {
            critQuery.where(wherePredicate);
        }
		
		Number count = em.createQuery(critQuery).getSingleResult();
		filter.setCalculatedTotalRecordsAndPages(count.intValue());
		
		logger.debug("countItems4Page() - items counted: [{}]", count);
	}
	
	private Predicate whereClauseForFilter(CriteriaBuilder critBuilder,
			CriteriaQuery<?> critQuery, Root<Invoice> root,
			InvoiceFilter filter) {
		
		if (filter == null) {
			return null;
		}
		
		List<Predicate> where = new ArrayList<Predicate>();
		
		if (filter.getId() != null) {
			where.add(critBuilder.equal(root.get(Invoice_.id), filter.getId()));
		}
		
		if (StringUtils.isNotEmpty(filter.getYear())) {
			where.add(critBuilder.equal(root.get(Invoice_.year), filter.getYear()));
		}
		
		if (filter.getNumber() != null) {
			where.add(critBuilder.equal(root.get(Invoice_.number), filter.getNumber()));
		}
		
		if (StringUtils.isNotEmpty(filter.getPurchaserName())) {
			where.add(critBuilder.like(
					root.<Purchaser>get("invoicePurchasers").<String>get("name"), "%" + filter.getPurchaserName() + "%"));
		}

        if (where.size() > 0) {
            return critBuilder.and(where.toArray(new Predicate[0]));
        } else {
            return null;
        }
	}

	@Override
	public Invoice getInvoice(Long id) throws ServiceException {
		logger.debug("getInvoice(id=[{}]) - start", + id);

		Invoice invoice = em.find(Invoice.class, id);
		recalculateInvoice(invoice);
		hrInvoiceNumberService.updateHRInvoiceNumber(invoice);

		logger.debug("getInvoice(Long) - end - return value=[{}]", invoice);
		return invoice;
	}

	public Invoice prepareNewInvoice() throws ServiceException {
		logger.debug("prepareNewInvoice() - start");
		
		Invoice invoice = null;
		if (this.invoiceTemplate != null) {
			try {
				invoice = invoiceTemplate.clone();
			} catch (CloneNotSupportedException e) {
				logger.warn("prepareNewInvoice()", e);
			}
		}
		
		if (invoice == null) {
			invoice = new Invoice();
		}
		
		invoice.setInvoicePositions(new ArrayList<InvoicePosition>());
		invoice.setInvoicePurchasers(new ArrayList<InvoicePurchaser>());
		
		Calendar nowDay = Calendar.getInstance();
		nowDay.set(Calendar.HOUR, 0);
		nowDay.set(Calendar.MINUTE, 0);
		nowDay.set(Calendar.SECOND, 0);
		nowDay.set(Calendar.MILLISECOND, 0);
		
		Long curYear = new Long(nowDay.get(Calendar.YEAR));
		invoice.setYear(curYear.toString());
		
		int maxInvoiceNumber = this.getMaxInvoiceNumber(curYear.toString());
		maxInvoiceNumber++;
		
		Long number = new Long(maxInvoiceNumber);
		invoice.setNumber(number);
		invoice.setDocumentDate(nowDay.getTime());
		invoice.setSoldDate(nowDay.getTime());
		
		PaymentKind pk = null;
		if (invoice.getPaymentKindId() != null) {
			pk = paymentKindsService.getPaymentKind(invoice.getPaymentKindId());
		}
		
		Date paymentDate = null;
		if (pk != null) {
			invoice.setPaymentKind(pk);
			paymentDate = paymentKindsService.calculatePaymentDate(nowDay.getTime(), pk.getId());
			invoice.setPaymentDate(paymentDate);
		}
		
		recalculateInvoice(invoice);
		hrInvoiceNumberService.updateHRInvoiceNumber(invoice);
		
		if (logger.isDebugEnabled()) {
			logger.debug("prepareNewInvoice() - end - return value=" + invoice);
		}
		return invoice;
	}

	protected int getMaxInvoiceNumber(String year) throws ServiceException {
		logger.debug("getMaxInvoiceNumber(year=[{}]) - start", year);

		CriteriaBuilder critBuilder = em.getCriteriaBuilder();
		CriteriaQuery<Invoice> critQuery = critBuilder.createQuery(Invoice.class);

		// select-clause
		Root<Invoice> root = critQuery.from(Invoice.class);
		critQuery.select(root)
				.where(critBuilder.equal(root.get(Invoice_.year), year))
				.orderBy(critBuilder.desc(root.get(Invoice_.number)));
		
		TypedQuery<Invoice> query = em.createQuery(critQuery).setMaxResults(1);
		
		Invoice invoice = null;
		try {
			invoice = query.getSingleResult();
		} catch (NoResultException ex) {
			logger.info("getMaxInvoiceNumber() - no result found - no invoices in this year?");
		}
		
		int maxNumber = 0;
		if (invoice != null) {
			maxNumber = invoice.getNumber().intValue();
		}
		
		logger.debug("getMaxInvoiceNumber() - end - return value=[{}]", maxNumber);
		return maxNumber;
	}

	public void saveInvoice(Invoice invoice) throws ServiceException {
		logger.debug("saveInvoice(Invoice invoice=[{}]) - start", invoice);

		List<InvoicePosition> positions = invoice.getInvoicePositions();
		if (positions != null) {
			for (InvoicePosition ip : positions) {
				ip.setInvoice(invoice);
			}
		}

		List<InvoicePurchaser> purchasers = invoice.getInvoicePurchasers();
		if (purchasers != null) {
			for (InvoicePurchaser ip : purchasers) {
				ip.setInvoice(invoice);
			}
		}

		em.merge(invoice);
		
		logger.debug("saveInvoice(Invoice) - end");
	}

	@Override
	public void updateInvoice(Invoice invoice) throws ServiceException {
		throw new UnsupportedOperationException("Ups... This method must be implemented!");
	}
	
	@Override
	public void addInvoicePosition(Invoice invoice, InvoicePosition invoicePosition)
			throws ServiceException {
		if (logger.isDebugEnabled()) {
			logger.debug("addInvoicePosition(Invoice invoice=" + invoice
					+ ", InvoicePosition invoicePosition=" + invoicePosition
					+ ") - start");
		}

		// read and set Tax object
		if (invoicePosition.getTaxId() != null) {
			Tax tax = taxesService.getTax(invoicePosition.getTaxId());
			invoicePosition.setTax(tax);
		}
		
		// set parent of this position
		invoicePosition.setInvoice(invoice);
		
		// add positions to parent's list
		List<InvoicePosition> invoicePositions = invoice.getInvoicePositions();
		if (invoicePositions == null) {
			invoicePositions = new ArrayList<InvoicePosition>();
			invoice.setInvoicePositions(invoicePositions);
		}
		invoicePositions.add(invoicePosition);

		this.recalculateInvoice(invoice);
		hrInvoiceNumberService.updateHRInvoiceNumber(invoice);
		
		if (logger.isDebugEnabled()) {
			logger.debug("addInvoicePosition(Invoice, InvoicePosition) - end");
		}
	}
	
	public void updateInvoicePosition(Invoice invoice,
			InvoicePosition invoicePosition, Long idx)
		throws ServiceException {
		if (logger.isDebugEnabled()) {
			logger.debug("updateInvoicePosition(Invoice invoice=" + invoice
					+ ", InvoicePosition invoicePosition=" + invoicePosition
					+ ", Long idx=" + idx + ") - start");
		}
		
		// read and set Tax object
		if (invoicePosition.getTaxId() != null) {
			Tax tax = taxesService.getTax(invoicePosition.getTaxId());
			invoicePosition.setTax(tax);
		}
		
		// set parent of this position
		invoicePosition.setInvoice(invoice);
		
		// add positions to parent's list
		List<InvoicePosition> invoicePositions = invoice.getInvoicePositions();
		if (invoicePositions == null) {
			invoicePositions = new ArrayList<InvoicePosition>();
			invoice.setInvoicePositions(invoicePositions);
		}
		invoicePositions.set(idx.intValue(), invoicePosition);

		this.recalculateInvoice(invoice);
		hrInvoiceNumberService.updateHRInvoiceNumber(invoice);

		if (logger.isDebugEnabled()) {
			logger.debug("updateInvoicePosition(Invoice, InvoicePosition, Long) - end");
		}
	}
	
	@Override
	public InvoicePosition getInvoicePosition(Invoice i, Long idx)
			throws ServiceException {
		
		InvoicePosition ip = null;
		
		if (i != null
				&& i.getInvoicePositions() != null
				&& i.getInvoicePositions().size() > idx) {
			ip = i.getInvoicePositions().get(idx.intValue());
		}
		
		return ip;
	}
	
	@Override
	public void addInvoicePurchaser(Invoice invoice, InvoicePurchaser invoicePurchaser)
			throws ServiceException {
		if (logger.isDebugEnabled()) {
			logger.debug("addInvoicePurchaser(Invoice invoice=" + invoice
					+ ", InvoicePurchaser invoicePurchaser=" + invoicePurchaser
					+ ") - start");
		}

		// read and set PurchaserRole object
		if (invoicePurchaser.getPurchaserRoleId() != null) {
			PurchaserRole role = purchasersRolesService.getPurchaserRole(invoicePurchaser.getPurchaserRoleId());
			invoicePurchaser.setPurchaserRole(role);
		}
		
		// set parent of this purchaser
		invoicePurchaser.setInvoice(invoice);
		
		// add purchaser to parent's list
		List<InvoicePurchaser> invoicePurchasers = invoice.getInvoicePurchasers();
		if (invoicePurchasers == null) {
			invoicePurchasers = new ArrayList<InvoicePurchaser>();
			invoice.setInvoicePurchasers(invoicePurchasers);
		}
		invoicePurchasers.add(invoicePurchaser);

		if (logger.isDebugEnabled()) {
			logger.debug("addInvoicePurchaser(Invoice, InvoicePurchaser) - end");
		}
	}
	
	public void updateInvoicePurchaser(Invoice invoice,
			InvoicePurchaser invoicePurchaser, Long idx)
		throws ServiceException {
		
		if (logger.isDebugEnabled()) {
			logger.debug("updateInvoicePurchaser(Invoice invoice=" + invoice
					+ ", InvoicePurchaser invoicePurchaser=" + invoicePurchaser
					+ ", Long idx=" + idx + ") - start");
		}
		
		// read and set PurchaserRole object
		if (invoicePurchaser.getPurchaserRoleId() != null) {
			PurchaserRole role = purchasersRolesService.getPurchaserRole(invoicePurchaser.getPurchaserRoleId());
			invoicePurchaser.setPurchaserRole(role);
		}
		
		// set parent of this purchaser
		invoicePurchaser.setInvoice(invoice);
		
		// set purchaser at parent's list
		List<InvoicePurchaser> invoicePurchasers = invoice.getInvoicePurchasers();
		if (invoicePurchasers == null) {
			invoicePurchasers = new ArrayList<InvoicePurchaser>();
			invoice.setInvoicePurchasers(invoicePurchasers);
		}
		invoicePurchasers.set(idx.intValue(), invoicePurchaser);

		if (logger.isDebugEnabled()) {
			logger.debug("updateInvoicePurchaser(Invoice, InvoicePurchaser, Long) - end");
		}
	}
	
	@Override
	public InvoicePurchaser getInvoicePurchaser(Invoice i, Long idx)
			throws ServiceException {
		
		InvoicePurchaser ip = null;
		
		if (i != null
				&& i.getInvoicePurchasers() != null
				&& i.getInvoicePurchasers().size() > idx) {
			ip = i.getInvoicePurchasers().get(idx.intValue());
		}
		
		return ip;
	}
	
	protected void recalculateInvoice(Invoice invoice) {
		if (logger.isDebugEnabled()) {
			logger.debug("recalculateInvoice(Invoice invoice=" + invoice + ") - start");
		}

		if (invoice == null)
			return;

		// create Map with summaries
		Map<Double, InvoicePositionSummary> calculatedSummaries =
			new HashMap<Double, InvoicePositionSummary>(5);

		// create List with keys of summaries
		List<Double> calculatedSummariesKeys = new ArrayList<Double>(4);
		
		TaxConfig tc = taxesConfigService.getTaxConfigByDate(invoice.getSoldDate());
		if (tc != null) {
			List<Double> availTaxes = tc.getAvailTaxes();
			for (Double at : availTaxes) {
				calculatedSummariesKeys.add(at);
				calculatedSummaries.put(at, new InvoicePositionSummary(at));
			}
		}
		calculatedSummariesKeys.add(NumberUtils.DOUBLE_ZERO);
		invoice.setCalculatedSummariesKeys(calculatedSummariesKeys);
		
		calculatedSummaries.put(NumberUtils.DOUBLE_ZERO,
				new InvoicePositionSummary(NumberUtils.DOUBLE_ZERO));
		
		// position with 'total' aggregates
		InvoicePositionSummary ip1 = new InvoicePositionSummary(NumberUtils.DOUBLE_ONE);
		calculatedSummaries.put(NumberUtils.DOUBLE_ONE, ip1);
		
		invoice.setCalculatedSummaries(calculatedSummaries);

		
		if (invoice.getInvoicePositions() != null) {
			for (InvoicePosition pos : invoice.getInvoicePositions()) {
				if (logger.isDebugEnabled()) {
					logger.debug("recalculateInvoice(Invoice) - iterate through invoicePositions - pos=" + pos);
				}
				
				Double percentTax = pos.getTax().getValue();
				if (logger.isDebugEnabled()) {
					logger.debug("recalculateInvoice(Invoice) - iterate through invoicePositions - percentTax=" + percentTax);
				}

				InvoicePositionSummary posFromMap =
					(InvoicePositionSummary) calculatedSummaries.get(percentTax);
				if (logger.isDebugEnabled()) {
					logger.debug("recalculateInvoice(Invoice) - iterate through invoicePositions - before add - posFromMap=" + posFromMap);
					logger.debug("recalculateInvoice(Invoice) - iterate through invoicePositions - before add  - ip1=" + ip1);
				}
				
				if (posFromMap != null) {
					posFromMap.addValueNetto(pos.getValueNetto());
					posFromMap.addSum(pos.getSum());
					posFromMap.addTotal(pos.getTotal());
				}
				
				ip1.addValueNetto(pos.getValueNetto());
				ip1.addSum(pos.getSum());
				ip1.addTotal(pos.getTotal());
				
				if (logger.isDebugEnabled()) {
					logger.debug("recalculateInvoice(Invoice) - iterate through invoicePositions - after add - posFromMap=" + posFromMap);
					logger.debug("recalculateInvoice(Invoice) - iterate through invoicePositions - after add  - ip1=" + ip1);
				}
			}
			
			// recalculate InvoicePositionSummary when dedicated service is set
			if (recalcTotalOfIPSService != null) {
				recalcTotalOfIPSService.recalculateTotal(ip1);
			}
			
			// get text for ip1::total
			BigDecimal total = ip1.getTotal();
			LexEngine le = new LexEnginePL();
			
			long integerPart = total.longValue();
			long digitsPart = total.multiply(new BigDecimal(100d)).remainder(new BigDecimal(100d)).longValue();
			
			invoice.setHrTotalIntegerPart(le.getTextForNumber(integerPart));
			invoice.setHrTotalDigitsPart(le.getTextForNumber(digitsPart));
			
			if (logger.isDebugEnabled()) {
				logger.debug("recalculateInvoice(Invoice) - set - hrTotalIntegerPart=" + invoice.getHrTotalIntegerPart());
				logger.debug("recalculateInvoice(Invoice) - set - hrTotalDigitsPart=" + invoice.getHrTotalDigitsPart());
			}
		}

		if (logger.isDebugEnabled()) {
			logger.debug("recalculateInvoice(Invoice) - end");
		}
	}
	
	public void deleteInvoice(Invoice invoice) throws ServiceException {
		logger.debug("deleteInvoice(invoice=[{}]) - start", invoice);

		em.remove(em.merge(invoice));

		logger.debug("deleteInvoice() - end");
	}
}
